#include <app.h>
#include <shader.h>
#include <cstdio>
#include <cstdarg>
#include <glm/glm.hpp>
#include <glm/gtc/type_ptr.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include "color_shader.hpp"
#include "quad.hpp"

static struct {
	glm::mat4 inverse_mvp;
	glm::vec3 camerapos;

	glm::vec3 raypos;
	glm::vec3 raydir;

	bool ishint;
} _params;

raycast::color_shader _colorshader;
raycast::quad _obj(glm::vec3(0.0f, 0.0f, 0.0f), 1.0f, 1.0f);
raycast::quad _plane(glm::vec3(0.0f, 0.0f, 0.0f), 5.0f, 5.0f);

// 将屏幕像素坐标转换成 NDC 坐标。屏幕坐标的原点是屏幕左上角。而 NDC 坐标原点是屏幕中心，y 轴指向屏幕上方。
static void
turn_screencoord_to_ndc(int width, int height, int x, int y, float *nx, float *ny) {
	  *nx = (float)x/(float)width * 2 - 1;
	  *ny = -((float)y/(float)height * 2 - 1); // reverte y axis
}

// 要想知道在窗口上点击时，是否点击到三维空间中的某个物体，需要先把窗口上点击时的二维坐标转换到三维空间，
// 具体会被转换成三维空间中的一条直线，然后判断该直线是否与物体相交。下面就是具体的实现。
// 把一个 2d ndc 转换成世界坐标空间中的一条射线。
static void
normalized2d_to_ray(float nx, float ny, glm::vec3 &raypos, glm::vec3 &raydir) {
	// 世界坐标 - 视图矩阵 - 透视矩阵 - 透视除法 - ndc
	// 要得到反转得到世界坐标，先需要视图矩阵和透视矩阵的反转矩阵

	// ndc 坐标系是左手坐标系，所以近平面的 z 坐标为远平面的 z 坐标要小
	glm::vec4 nearpoint_ndc(nx, ny, -1, 1);
	glm::vec4 nearpoint_world = _params.inverse_mvp * nearpoint_ndc;

	// 消除矩阵反转后，透视除法的影响
	nearpoint_world /= nearpoint_world.w;
	
	raypos = glm::vec3(nearpoint_world);
	raydir = raypos - _params.camerapos;
	raydir = glm::normalize(raydir);
}

static void 
cb_mouse(GLFWwindow *wnd, int buttn, int action, int mods) {
	if (action == GLFW_PRESS) {
		double sx, sy;
		glfwGetCursorPos(wnd, &sx, &sy);
		int w, h;
		glfwGetWindowSize(wnd, &w, &h);

		float nx, ny;
		turn_screencoord_to_ndc(w, h, (int)sx, (int)sy, &nx, &ny);

		glm::vec3 raypos, raydir;
		normalized2d_to_ray(nx, ny, raypos, raydir);

		if (_obj.isintersect(raypos, raydir))
			_params.ishint = true;
	} else {
		_params.ishint = false;
	}
}

static void
cb_cursor_pos(GLFWwindow *wnd, double x, double y) {
	if (!_params.ishint)
		return;

	double sx, sy;
	glfwGetCursorPos(wnd, &sx, &sy);
	int w, h;
	glfwGetWindowSize(wnd, &w, &h);

	float nx, ny;
	turn_screencoord_to_ndc(w, h, (int)sx, (int)sy, &nx, &ny);

	glm::vec3 raypos, raydir;
	normalized2d_to_ray(nx, ny, raypos, raydir);

	glm::vec3 pos;
	if (_plane.intersect(raypos, raydir, pos)) {
		glm::vec3 center = pos;
		_obj.move(center);
	}
}

static GLboolean
cb_init(GLFWwindow *wnd) {
	glfwSetMouseButtonCallback(wnd, cb_mouse);
	glfwSetCursorPosCallback(wnd, cb_cursor_pos);

	_params.camerapos = glm::vec3(0.0f, 2.0f, 3.0f);

	glEnable(GL_DEPTH_TEST);

	if (!_colorshader.init())
		return GL_FALSE;
	_obj.init();
	_plane.init();

    return GL_TRUE;
}

static void
cb_render(GLFWwindow *wnd, double current, int w, int h) {
    static GLfloat black[] = {0, 0, 0, 0};
    glClearBufferfv(GL_COLOR, 0, black);
    static GLfloat one = 1.0f;
    glClearBufferfv(GL_DEPTH, 0, &one);

    glm::mat4 viewmat = glm::lookAt(_params.camerapos, glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3(0.0f, 1.0f, 0.0f));
    glm::mat4 projmat = glm::perspective(45.0f, (float)w / (float)h, 1.0f, 100.0f);
    glm::mat4 mvp = projmat * viewmat;
    _params.inverse_mvp = glm::inverse(mvp);

    _colorshader.useprogram();
    
    // 先绘制背景，并不写入深度缓冲中，现在缓冲中的值仍都是最小值 1.0f ，这样之后的绘制就会覆盖背景，防止重叠造成闪烁。
    glDepthMask(GL_FALSE);
    if (_params.ishint) {
    	_colorshader.setuniform(mvp, 0.0f, 0.0f, 1.0f);
    	_plane.draw();
    }
    glDepthMask(GL_TRUE);

    _colorshader.setuniform(mvp, 1.0f, 0.0f, 0.0f);
    _obj.draw();

    fflush(stdout);
}

static void
cb_destroy() {
    
}

int 
main() {
	if (!app_init(800, 600, "raycast", cb_init))
		return -1;

	app_run(cb_render);
	app_destroy(cb_destroy);

	return 0;
}
